package ch.fhnw.apsi.beans;

import static java.util.Collections.unmodifiableSortedMap;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;

public class BeanHelper {

  /**
   * Returns an TreeMap of all values. Later changes to the bean will not influence this Map in any way.
   * 
   * @param bean
   *          Any Java bean.
   * @return Map of all fields and values.
   */
  public static SortedMap<String, Object> beanToMap(@Nonnull final Object bean) {
    try {
      final BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
      final PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
      final SortedMap<String, Object> map = new TreeMap<>();
      for (final PropertyDescriptor propDesc : descriptors) {
        final Method getter = propDesc.getReadMethod();
        if (getter.getDeclaringClass() == Object.class)
          continue; // this ignores getClass()
        map.put(propDesc.getName(), getter.invoke(bean));
      }
      return map;
    } catch (final Exception e) {
      throw new RuntimeException("Can't process bean!");
    }
  }

  /**
   * This map returns a string representation of each value.
   * 
   * @see #beanToMap(Object)
   */
  @SuppressWarnings({ "rawtypes", "unchecked" })
  public static Map<String, String> beanToStringMap(@Nonnull final Object bean) {
    final Map<String, Object> objMap = beanToMap(bean);
    for (final Entry<String, Object> entry : objMap.entrySet())
      entry.setValue(Objects.toString(entry.getValue(), ""));
    final Map map = objMap; // generic down-cast
    return map;
  }

  /**
   * Fill parameter values of a map into a given java bean.
   * 
   * 
   * @param map
   *          The map with the values.
   * @param bean
   *          The bean.
   * @param ignore
   *          Annotation that is used on the fields that should be ignored (must be in the getter).
   * @return The returned map contains all Exceptions that were thrown, if any. They are mapped to the field names. An
   *         empty map indicates that everything went well.
   */
  @Nonnull
  @CheckReturnValue
  public static SortedMap<String, Exception> mapToBean(final Map<String, String> map, final Object bean,
      final Class<? extends Annotation> ignore) {
    final BeanInfo beanInfo;
    final SortedMap<String, Exception> result = new TreeMap<>();
    try {
      beanInfo = Introspector.getBeanInfo(bean.getClass());
    } catch (final IntrospectionException e1) {
      result.put("[Introspector]", e1);
      return unmodifiableSortedMap(result);
    }
    final PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
    for (final PropertyDescriptor propDesc : descriptors)
      try {
        final Method getter = propDesc.getReadMethod();
        if (getter.getDeclaringClass() == Object.class || getter.getAnnotation(ignore) != null)
          continue;
        final Method setter = propDesc.getWriteMethod();
        if (setter == null)
          continue;
        final Class<?> type = propDesc.getPropertyType();
        // At this point the parameter value is a String or null:
        Object value = map.get(propDesc.getName());
        if (value == null)
          throw new Exception("Der Parameter ist nicht definiert.");
        // Now let's check is it it should be a String:
        else if (type != String.class && type != CharSequence.class)
          // primitive types have a valueOf(String)-Method.
          value = type.getMethod("valueOf", String.class).invoke(null, value);
        setter.invoke(bean, value);
      } catch (final InvocationTargetException e) {
        if (e.getCause() instanceof Exception)
          result.put(propDesc.getName(), (Exception) e.getCause());
        else
          result.put(propDesc.getName(), e);
      } catch (final Exception e) {
        result.put(propDesc.getName(), e);
      }
    return unmodifiableSortedMap(result);
  }

}
